/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ThumbsDown.
 *
 * The Initial Developer of the Original Code is
 * Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


var EXPORTED_SYMBOLS =
[
    "NS_HTML",
    "NS_XUL",
    "getStringBundleGetter",
    "extend",
    "arrayWithValue",
    "arrayEquals",
    "indexOf",
    "count",
    "unique",
    "replace",
    "escapeRegExp",
    "createElement",
    "clearListbox",
    "getParents",
    "getCommonParent",
    "SelectionIterator",
    "NodeIterator",
    "loadContent",
    "openUrls",
    "openWindow",
    "schedule",
    "initKey",
    "callLater",
    "Timer",
    "Box",
    "TreeView",
    "FilteredTreeView",
    "levenshtein",
];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");


const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
const NS_HTML = "http://www.w3.org/1999/xhtml"


function getStringBundleGetter(stringBundle)
{
    // is <xul:stringbundle> ?
    if (stringBundle.stringBundle)
        stringBundle = stringBundle.stringBundle;
    
    return function(key)
    {
        if (arguments.length > 1)
        {
            var params = Array.prototype.slice.call(arguments, 1);
            return stringBundle.formatStringFromName(key, params, params.length);
        }
        else
            return stringBundle.GetStringFromName(key);
    };
}

// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance
function extend(child, supertype)
{
   child.prototype.__proto__ = supertype.prototype;
}

function arrayWithValue(length, value)
{
    var arr = [];
    while (length-- > 0)
        arr.push(value);
    return arr;
}

function arrayEquals(a, b, func)
{
    if (a.length !== b.length)
        return false;
    
    var n = a.length;
    if (func)
    {
        for (var i = 0; i < n; i++)
            if (! func(a[i], b[i]))
                return false;
    }
    else
    {
        for (var i = 0; i < n; i++)
            if (a[i] !== b[i])
                return false;
    }
    
    return true;
}

function indexOf(arr, func)
{
    for (var i = 0; i < arr.length; i++)
        if (func(arr[i]))
            return i;
    return -1;
}

function count(arr, callback)
{
    if (callback === undefined)
        callback = function(b) { return (b); }
    var count = 0;
    for each (var a in arr)
        if (callback(a))
            count++;
    return count;
}

function unique(list, equals)
{
    let result = [];
    equals = equals || function(a, b) { return (a == b); };
    
    loop:
    for each (let a in list)
    {
        for each (let b in result)
            if (equals(a, b))
                continue loop;
        result.push(a);
    }
    
    return result;
}

function replace(template, replacements)
{
    var replacer = function(substring)
    {
        var name = substring.substr(2, substring.length - 3);
        if (name in replacements)
            return replacements[name];
        return substring;
    };
    return template.replace(/\$\{\w+\}/g, replacer);
}

function escapeRegExp(s)
{
    return s.replace(/([.*+?^${}()|[\]\\])/g, '\\$1');
}

function createElement(document, namespace, name, attrs, children)
{
    var element = document.createElementNS(namespace, name);
    if (attrs)
        for (let [attrName, attrValue] in Iterator(attrs))
            element.setAttribute(attrName, attrValue);
    if (children)
        for each (var child in children)
            element.appendChild(child);
    return element;
}

function clearListbox($listbox)
{
    while ($listbox.itemCount > 0)
        $listbox.removeItemAt(0);
}

function getParents($element)
{
    if ($element)
    {
        let parents = getParents($element.parentNode);
        parents.push($element);
        return parents;
    }
    else
        return [];
}

function getCommonParent($element1, $element2)
{
    let parents1 = getParents($element1);
    let parents2 = getParents($element2);
    
    let n = Math.min(parents1.length, parents2.length);
    let i;
    for (i = 0; i < n; i++)
        if (parents1[i] != parents2[i])
            break;
    
    return ((i > 0) ? parents1[i - 1] : null);
}

function SelectionIterator(selection)
{
    var rangeCount = selection.getRangeCount();
    for (var i = 0; i < rangeCount; i++)
    {
        var start = {};
        var end = {};
        selection.getRangeAt(i, start, end);
        for (var k = start.value; k <= end.value; k++)
            yield k;
    }
}

function NodeIterator(nodeList)
{
    for (let i = 0; i < nodeList.length; i++)
        yield nodeList[i];
}

function loadContent(url)
{
    var stream = Cc["@mozilla.org/scriptableinputstream;1"].getService(Ci.nsIScriptableInputStream);
    var channel = Services.io.newChannel(url, null, null);
    var input = channel.open();
    stream.init(input);
    var content = stream.read(input.available());
    stream.close();
    input.close();
    return content;
}

function openUrls(urls)
{
    var win = Services.wm.getMostRecentWindow("navigator:browser");
    if (! win)
        return;
    
    var browser = win.getBrowser();
    let uniqueUrls = unique([url for each (url in urls) if (url)]);
    let firstTab = null;
    
    for each (let url in uniqueUrls)
    {
        let tab = browser.addTab(url);
        firstTab = firstTab || tab;
    }
    
    if (firstTab)
    {
        browser.selectedTab = firstTab;
        win.focus();
    }
}

function openWindow({parent, url, name, features, args, callback, focus})
{
    var win = (name ? Services.wm.getMostRecentWindow(name) : null);
    
    if (win)
    {
        if (callback)
            callback(win);
        if (focus || (focus == undefined))
            win.focus();
    }
    else
    {
        let winArgs = null;
        
        if (Array.isArray(args))
        {
            winArgs = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
            for each (let arg in args)
            {
                var variant = Cc["@mozilla.org/variant;1"].createInstance(Ci.nsIWritableVariant);
                variant.setFromVariant(arg);
                winArgs.appendElement(variant, false);
            }
        }
        else if (args && (typeof(args) == "object"))
        {
            winArgs = args;
            winArgs.wrappedJSObject = args;
        }
        
        win = Services.ww.openWindow(parent, url, name, features, winArgs);
    }
    
    return win;
}

function schedule(obj, func, args, delay, type)
{
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    delay = ((delay !== undefined) ? delay : 0);
    type = ((type !== undefined) ? type : timer.TYPE_ONE_SHOT);
    var callback = {notify: function(timer) { func.apply(obj, args); }};
    timer.initWithCallback(callback, delay, type);
    return timer;
}

function initKey(key, prefName)
{
    return new DynamicKey(key, prefName);
}


/**
 * Binds preference values to <xul:key> shortcut key.
 * Changes to prefs are updated immediately to the UI.
 */
// http://forums.mozillazine.org/viewtopic.php?f=19&t=2048501
function DynamicKey(key, prefName)
{
    this.key = key;
    this.prefs = Services.prefs.getBranch(prefName + ".");
    this.prefs.addObserver("", this, false);
    
    this.reset();
    
    // clean up on window close
    var win = key.ownerDocument.defaultView;
    var self = this;
    win.addEventListener("unload", function() { self.dispose(); }, false);
}

DynamicKey.prototype =
{
    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
    
    // nsIObserver
    observe: function(subject, topic, data)
    {
        this.reset();
    },
    
    reset: function()
    {
        var spec = {};
        var has = {};
        
        for each (var name in ["modifiers", "key", "keycode"])
        {
            spec[name] = this.prefs.getComplexValue(name, Ci.nsISupportsString).data.trim();
            has[name] = (spec[name].length != 0);
        }
        
        // disable key if key spec is invalid
        var disabled = (! ((has.modifiers && has.key) || has.keycode));
        this.key.setAttribute("disabled", disabled); 
        
        // update key shortcut attributes
        for each (var name in ["modifiers", "key", "keycode"])
            if (disabled)
                this.key.removeAttribute(name);
            else
                this.key.setAttribute(name, spec[name]);
        
        // reset keyset so that key attribute changes take effect
        var keyset = this.key.parentNode;
        keyset.parentNode.insertBefore(keyset, keyset.nextSibling);
        
        // reset shortcut key text in menus
        var doc = this.key.ownerDocument;
        var nodeList = doc.querySelectorAll("menuitem[key='" + this.key.id + "']");
        for (var i = 0; i < nodeList.length; i++)
        {
            nodeList[i].setAttribute("acceltext","");
            nodeList[i].removeAttribute("acceltext");
        }
    },
    
    dispose: function()
    {
        this.prefs.removeObserver("", this);
        
        delete this.key;
        delete this.prefs;
    }
}


function callLater(delay, obj, func/*, args...*/)
{
    if (! func)
        throw new Error("callLater: function is " + func);
    
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var args = Array.prototype.slice.call(arguments, 3);
    var callback = {notify: function() { func.apply(obj, args); }}
    timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT);
    return timer;
}


function Timer(precise, delay, obj, func, args)
{
    this.type = (precise ? Ci.nsITimer.TYPE_REPEATING_PRECISE : Ci.nsITimer.TYPE_REPEATING_SLACK);
    this.delay = delay;
    this.callback = {notify: function(timer) { func.apply(obj, args); }};
    this.timer = null;
}

Timer.prototype =
{
    start: function(force)
    {
        if (this.timer)
            if (force)
                this.stop();
            else
                throw new Error("Timer already started");
        this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        this.timer.initWithCallback(this.callback, this.delay, this.type);
    },
    
    stop: function()
    {
        if (this.timer)
        {
            this.timer.cancel();
            this.timer = null;
        }
    },
    
    trigger: function(noReInit)
    {
        if (! noReInit)
            this.stop();
        this.callback.notify(null);
        if (! noReInit)
            this.start();
    }
}


function Box(/*box; width, height*/)
{
    var n = arguments.length;
    var width  = ((n == 1) ? arguments[0].width  : arguments[0]);
    var height = ((n == 1) ? arguments[0].height : arguments[1]);
    
    this.width = width;
    this.height = height;
}

Box.prototype =
{
    contains: function(box, partially)
    {
        if (partially)
            return ((box.width <= this.width) || (box.height <= this.height));
        else
            return ((box.width <= this.width) && (box.height <= this.height));
    },
    
    enlarge: function(box, partially)
    {
        if (! this.contains(box, partially))
        {
            this.width  = (partially ? ((box.width > box.height) ? this.width : box.width)   : Math.max(this.width,  box.width));
            this.height = (partially ? ((box.width > box.height) ? box.height : this.height) : Math.max(this.height, box.height));
        }
        
        return this;
    },
    
    shrink: function(box, partially)
    {
        if (! box.contains(this, partially))
        {
            this.width  = (partially ? ((box.width > box.height) ? box.width : this.width)   : Math.min(this.width,  box.width));
            this.height = (partially ? ((box.width > box.height) ? this.height : box.height) : Math.min(this.height, box.height));
        }
        
        return this;
    },
    
    toString: function()
    {
        return "[" + this.width + ", " + this.height + "]";
    }
}


// nsITreeView
function TreeView(model)
{
    this.model = model;
}

TreeView.prototype =
{
    QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
    
    // nsITreeView
    get rowCount() { return this.model.length; },
    setTree: function(treeBoxObject) { this.treeBoxObject = treeBoxObject; },
    getCellText: function(row, col) { return this.model[row]; },
    isContainer: function(row) { return false; },
    isSeparator: function(row) { return false; },
    isSorted: function() { return false; },
    getLevel: function(row) { return 0; },
    getImageSrc: function(row, col) { return null; },
    getRowProperties: function(row) { return ""; },
    getCellProperties: function(row, col) { return ""; },
    getColumnProperties: function(colid, col) { return ""; }
}


function FilteredTreeView(model, filter) // extend TreeView
{
    this.original = model;
    this._filter = filter;
    this.index2row = new Array(this.original.length);
    
    TreeView.call(this, this._updateModel());
}

FilteredTreeView.prototype =
{
    get originalCount() { return this.original.length; },
    
    get filter() { return this._filter; },
    set filter(value)
    {
        if (value != this._filter)
        {
            this._filter = value;
            this._updateView();
        }
        
        return value;
    },
    
    isIndexFiltered: function(index)
    {
        return (this.index2row[index] === null);
    },
    
    getIndexRow: function(index)
    {
        return this.index2row[index];
    },
    
    _updateModel: function()
    {
        var filter = this.filter;
        var original = this.original;
        var index2row = this.index2row;
        var model = null;
        
        if (filter)
        {
            model = [];
            var row = 0;
            for (var index = 0; index < original.length; index++)
            {
                var element = original[index];
                var shown = (! filter(element));
                if (shown)
                    model.push(element);
                index2row[index] = (shown ? row++ : null);
            }
        }
        else
        {
            model = original;
            for (var index = 0; index < original.length; index++)
                index2row[index] = index;
        }
        
        return this.model = model;
    },
    
    _updateView: function()
    {
        // original, source (old model), destination (new model)
        var org = this.original;
        var src = this.model;
        var dst = this.model = this._updateModel();
        
        var org_n = org.length;
        var src_n = src.length;
        var dst_n = dst.length;
        
        if ((dst_n == src_n) && ((dst_n == 0) || (dst_n == org_n) || arrayEquals(dst, src)))
            return;
        
        var boxObject = this.treeBoxObject;
        var selection = this.selection;
        
        var selectEventsSuppressed = selection.selectEventsSuppressed;
        selection.selectEventsSuppressed = true;
        boxObject.beginUpdateBatch();
        
        var org_i = org_n - 1;
        var src_i = src_n - 1;
        var dst_i = dst_n - 1;
        
        while ((src_i >= 0) && (dst_i >= 0))
        {
            var org_e = org[org_i];
            var src_e = src[src_i];
            var dst_e = dst[dst_i];
            
            var in_src = (src_e === org_e);
            var in_dst = (dst_e === org_e);
            
            if (in_src != in_dst)
                if (in_src)
                    boxObject.rowCountChanged(src_i, -1);
                else
                    boxObject.rowCountChanged(src_i + 1, 1);
            
            org_i--;
            if (in_src) src_i--;
            if (in_dst) dst_i--;
        }
        
        if (src_i >= 0)
            boxObject.rowCountChanged(0, -(src_i + 1));
        if (dst_i >= 0)
            boxObject.rowCountChanged(0, (dst_i + 1));
        
        boxObject.endUpdateBatch();
        selection.selectEventsSuppressed = selectEventsSuppressed;
    }
}

extend(FilteredTreeView, TreeView);


// http://webreflection.blogspot.com/2009/02/levenshtein-algorithm-revisited-25.html
var levenshtein = function(min, split){
    // Levenshtein Algorithm Revisited - WebReflection
    try{split=!("0")[0]}catch(i){split=true};
    return function(a, b){
        if(a == b)return 0;
        if(!a.length || !b.length)return b.length || a.length;
        if(split){a = a.split("");b = b.split("")};
        var len1 = a.length + 1,
            len2 = b.length + 1,
            I = 0,
            i = 0,
            d = [[0]],
            c, j, J;
        while(++i < len2)
            d[0][i] = i;
        i = 0;
        while(++i < len1){
            J = j = 0;
            c = a[I];
            d[i] = [i];
            while(++j < len2){
                d[i][j] = min(d[I][j] + 1, d[i][J] + 1, d[I][J] + (c != b[J]));
                ++J;
            };
            ++I;
        };
        return d[len1 - 1][len2 - 1];
    }
}(Math.min, false);
